#!/usr/bin/env bash
#
# run given command via Docker on all distros supported by ReaR, or only a single one

# command line args:
#
# $0 [os ...] -- <command> [args ...]
#
# Note: This script works also on MacOS and makes an effort to use cross-platform
# compatible options for tools like cp

# Define the list of supported images
declare -r IMAGES=(
    ubuntu:{20.04,22.04,24.04}
    debian:{11,12,unstable}
    opensuse/leap:15
    registry.suse.com/suse/sle15
    centos:8 # discontinued
    quay.io/centos/centos:stream{9,10}
    # registry.access.redhat.com/ubi{7,8,9} basic packages like parted are missing
    fedora:{41,42} # rawhide Docker image is broken, see https://github.com/fedora-cloud/docker-brew-fedora/issues/109
    archlinux
    manjarolinux/base
)

# images using x86_64-v3 architecture which doesn't work on M1 Mac Rosetta
# this is regex for grep, remember that MacOS grep is different from Linux grep!
AMD64_V3_IMAGE_FILTER="centos:stream10"

declare -r HELP_TEXT="
$0 [image ...] -- <command> [args ...]
specify image patterns or omit image to run in all supported images:

$(fold -sw 60 <<<"${IMAGES[*]}")

You can also specify completely different Docker images instead.

Without command it will show the Bash version in all images

Special commands:

--patch     Patch the (given or all) images to contain the ReaR build and run dependencies
            Add --continue-and-record-successful FILE to continue after a failure and record
            the successful images in FILE
-i          Interactive shell, used by default if image selection yields only a single image

Architecture defaults to the host platform, specify architecture via
-a <architecture>, e.g. -a amd64 on M1 Mac as part of the image selection

Note: On M1 Mac we automatically remove incompatible Docker images if amd64 is set!
"

# Try podman when docker is missing.  This must be at the top because aliases
# are expanded when a function definition is read, not when it is executed.
if ! type -a docker &>/dev/null && type -a podman &>/dev/null; then
    shopt -s expand_aliases
    alias docker=podman
fi

function gh_actions_output {
    if test "$GITHUB_ACTIONS"; then
        echo "$*"
    fi
}

function die {
    gh_actions_output "::error::$*"
    echo -e "ERROR: $*" 1>&2
    exit 1
}

function exit_handler {
    echo "** SCRIPT RUN TIME $SECONDS SECONDS **"
}
trap exit_handler EXIT

extra_docker_args=()
use_images=()

# patch the images to contain all ReaR build and run dependencies
function patch_images() {
    local continue_and_record_successful_file=""
    while test $# -gt 0; do
        case "$1" in
        --continue-and-record-successful)
            test $# -lt 2 && die "Missing filename for $1 argument"
            continue_and_record_successful_file="$2"
            rm -vf "$continue_and_record_successful_file"
            shift 2
            ;;
        *)
            die "Unknown option $1"
            ;;
        esac
    done

    for image in "${use_images[@]}"; do
        gh_actions_output "::group::Patching $image"
        printf "********** PATCHING %-40s **********\n" "$image" 1>&2
        read -r oldsize junk < <(docker images --format '{{.VirtualSize}}' "$image")
        docker buildx build \
            --tag "$image" \
            --build-arg "IMAGE=$image" \
            "${extra_docker_args[@]}" \
            tools
        local docker_build_ret=$?
        if test "$continue_and_record_successful_file"; then
            if test $docker_build_ret -eq 0; then
                echo "$image" >>"$continue_and_record_successful_file"
            else
                printf "********** FAILED   %-40s **********\n" "$image" 1>&2
                continue
            fi
        else
            test $docker_build_ret -eq 0 || die "Failed building $image"
        fi
        read -r newsize junk < <(docker images --format '{{.VirtualSize}}' "$image")
        test "$oldsize" != "$newsize" && printf "********** %-35s %7s -> %-7s *****\n" "$image" "$oldsize" "$newsize"
        gh_actions_output "::endgroup::"
    done
}

command_args=()
while test $# -gt 0; do
    case "$1" in
    -a)
        echo "Using architecture $2 instead of default Docker architecture $(docker system info --format '{{.Architecture}}')"
        extra_docker_args+=("--platform" "linux/$2")
        shift 2
        ;;
    -h | --help)
        echo "$HELP_TEXT"
        exit 1
        ;;
    --)
        shift
        [[ $# -gt 0 ]] && command_args=("$@")
        break
        ;;
    *)
        #shellcheck disable=SC2207
        use_images+=($(
            ((c = 0))
            for image in "${IMAGES[@]}"; do
                if [[ "$image" == *$1* ]]; then
                    echo "$image"
                    ((c++))
                fi
            done
            if ((c == 0)); then
                echo "$1"
            fi
        ))
        shift
        ;;
    esac
done

if test ${#use_images[@]} -eq 0; then
    use_images=("${IMAGES[@]}")
fi

if uname -a | grep -q "Darwin.*arm64"; then
    if echo "${extra_docker_args[*]}" | grep -q amd64; then
        echo "M1 Mac detected and amd64 architecture specified, removing incompatible Docker images matching $AMD64_V3_IMAGE_FILTER"
        mapfile -t use_images < <(
            printf "%s\n" "${use_images[@]}" | grep -v -E "$AMD64_V3_IMAGE_FILTER"
        )
    fi
fi

# Centos/stream10 requires x86-64-v3 or higher to be useful
if [[ -x /lib64/ld-linux-x86-64.so.2 ]] ; then
    /lib64/ld-linux-x86-64.so.2 --help 2>&1 | grep supported | grep -qE '(x86-64-v3|x86-64-v4)'
    if [[ $? -eq 1 ]] ; then
       echo "Hardware architecture not supported, removing incompatible Docker images matching $AMD64_V3_IMAGE_FILTER"
       mapfile -t use_images < <(
                printf "%s\n" "${use_images[@]}" | grep -v -E "$AMD64_V3_IMAGE_FILTER"
            )
    fi
fi

if test ${#command_args[@]} -eq 0; then
    if test ${#use_images[@]} -eq 1; then # if only one image is given and no command then go interactive
        command_args=(-i)
    else
        command_args=("echo" "Bash is \$BASH_VERSION")
    fi
fi

rear_toplevel_dir=$(dirname "$(dirname "$(readlink -f "$0")")")
bash_script=tools/run-in-docker-script.sh
bash_args=(-c "echo BUG: CONFIGURATION ERROR")

# declare -p IMAGES use_images command_args rear_toplevel_dir ; exit 0

case "${command_args[0]}" in
--patch)
    patch_images "${command_args[@]:1}"
    exit $?
    ;;
-i)
    bash_args=(-i)
    ;;
-*)
    bash_args=("${command_args[@]}")
    ;;
*)
    echo "${command_args[*]}" >"$rear_toplevel_dir/$bash_script"
    bash_args=("$bash_script")
    ;;
esac

# set Docker interactive if we have interactive terminal
test -t 0 && extra_docker_args+=(-i -t)

# Note: bash reads --rcfile <file> for interactive shells
#       bash reads the file specified in BASH_ENV for non-interactive shells
#       we set both so that our startup file run-in-docker.bashrc is ALWAYS read as it sets the PATH
for image in "${use_images[@]}"; do
    gh_actions_output "::group::Running $image"
    printf "\n********** %-40s **********\n" "$image" 1>&2
    image_name="$(echo -n "$image" | tr -cs '0-9a-zA-Z-_' -)"
    dist_dest="dist-all/$image_name"
    mkdir -p "$rear_toplevel_dir/$dist_dest" || die "Could not mkdir $rear_toplevel_dir/$dist_dest"
    docker run \
        --rm \
        --sig-proxy=false \
        -h "$image_name" \
        -v "$rear_toplevel_dir:/rear" \
        -e REAR_VAR=/tmp/rear \
        -e BASH_ENV=tools/run-in-docker.bashrc \
        -w /rear \
        "${extra_docker_args[@]}" \
        "$image" \
        /bin/bash --rcfile tools/run-in-docker.bashrc "${bash_args[@]}" || die "############### DOCKER RUN FAILED FOR $image"
    if test "$(ls -l "$rear_toplevel_dir/dist" 2>/dev/null || :)"; then
        echo "********** Copying dist to ${dist_dest}"
        cp -R -x "$rear_toplevel_dir/dist/." "$rear_toplevel_dir/$dist_dest/" || die "Could not copy dist to $dist_dest"
    fi
    gh_actions_output "::endgroup::"
done
